#!/usr/bin/python
# Copyright 2009 Alex K (wtwf.com) All rights reserved.
# $Id: prevnext.py,v 1.35 2009-01-07 21:27:56 ark Exp $

"""%(PROGRAM)s works out some previous/next links

-d
  debug output
-f
  force execution
-b base url
  the base url for the blog (e.g. /blog)
-s=individual,archive,labels,index,calendar
  --skip things to skip creating
-a dir
  --archive directory
"""

# latest version available http://wtwf.com/scripts/bin/expandRss
# documented somewhere around http://wtwf.com/scripts/

# TODO(ark) this http://www.osxfaq.com/rss/osxfaq.xml

import sys
import os
import re
import logging
import datetime
import getopt
import stat

if sys.version_info < (2, 4):
    raise "You probably need python 2.5 or greater"

def usage(code, msg=''):
  if code:
    fd = sys.stderr
  else:
    fd = sys.stdout
  PROGRAM = os.path.basename(sys.argv[0])
  print >> fd, __doc__ % locals()
  if msg:
    print >> fd, msg
  sys.exit(code)

def GetMonths():
  months = {}
  for i in range(1,13):
    months[datetime.date(2008, i, 1).strftime('%B')] = i
  return months

# key is abs directory, value is ordered list of files
file_stats = {}

YEAR_MONTH_RE = re.compile("/([12]\d\d\d/\d\d)(/|$)")

ARCHIVE_DIRS = re.compile("/archives?(/|$)")

FILENAME_DATE_RE = re.compile("/(\d\d\d\d)_([01]\d)_([012]\d)_[^/]*$")

BLOG_DATE_HEADER_RE = re.compile('(<h3 class="blogdateheader">'
                                 '|<h2 class="date-header">'
                                 '|<h3>).*, '
                                 '(\S+) (\d+), (\d\d\d\d)</h[23]>')

BYLINE_RE = re.compile('(<div class="byline">.*@ '
                       '|title="permanent link">)(\d\d?):(\d\d) ([AP]M)')

MONTHS = GetMonths()

BASE_FILENAME = os.path.abspath('.')
BASE_URL = ''
FLAG_SKIP = []
ARCHIVE_DIR = 'archive'

def GetUrl(file_name):
  return file_name.replace(BASE_FILENAME, BASE_URL)

def WritePrevNexts(files, skip_first=False):
  for i in range(len(files)):
    if i == 0 and skip_first:
      continue
    fn = files[i].replace('.php', '.prevnext')
    logging.debug("Writing: %s", fn)
    fnout = open(fn, 'w')
    if i > 0:
      fnout.write('<div class="prev"><a href="%s">'
                  '&lt;&lt; Previous Post</a></div>\n' %
                  GetUrl(files[i - 1]))
    if i < len(files) - 1:
      fnout.write('<div class="next"><a href="%s">'
                  'Next Post &gt;&gt;</a></div>\n' %
                  GetUrl(files[i + 1]))
    fnout.write('<div class="prevnext"></div>\n')
    fnout.close()

def GetFileStatsFor(directory):
  for (dirpath, _, filenames) in os.walk(directory):
    GetDirectoryStats(dirpath, filenames)

  file_dates = FILE_DATE_CACHE.items()
  file_dates.sort(CmpDates)
  # now do the special index.php one...
  if 'index' not in FLAG_SKIP:
    logging.info("Doing index.php")
    d = GetFileDate("index.php")
    if d:
      for i in range(len(file_dates)):
        if d <= file_dates[i][1]:
          WritePrevNexts([file_dates[i - 1][0],
                          os.path.abspath("index.php")], skip_first=True)
          break

  # write all the file date cache prevnext files
  if 'individual' not in FLAG_SKIP:
    if not file_dates:
      logging.warning("Unable to find file dates for individual files.\n"
                      "Try tweaking the following regular expressions:\n"
                      "BLOG_DATE_HEADER_RE and BYLINE_RE")
    else:
      logging.info("Doing %d Individual files", len(file_dates))
      file_dates = [x[0] for x in file_dates]
      WritePrevNexts(file_dates)

def GetDirectoryStats(dirname, names):
  dirname = os.path.abspath(dirname)
  phps = [x for x in names if x.endswith(".php") and x != 'index.php']
  if YEAR_MONTH_RE.search(dirname):
    # logging.debug("Filling up Cache for: %s", dirname)
    for x in phps:
      GetFileDate(os.path.join(dirname, x))
  if ARCHIVE_DIRS.search(dirname):
    phps.sort(lambda x,y: CmpFiles(dirname, x, y))
    if 'archive' not in FLAG_SKIP:
      logging.info("Doing Archive Directory: %s", dirname)
      WritePrevNexts([os.path.join(dirname, x) for x in phps])
    if 'calendar' not in FLAG_SKIP:
      WriteCalendar(phps)

def CmpFiles(dirname, x, y):
  x = GetFileDate(os.path.join(dirname, x))
  y = GetFileDate(os.path.join(dirname, y))

  if x and y:
    td = x - y
    return (td.days * 24 * 60 * 60) + td.seconds
  else:
    return 0

def CmpDates(x, y):
  x = x[1]
  y = y[1]
  if x and y:
    td = x - y
    return (td.days * 24 * 60 * 60) + td.seconds
  else:
    return 0

FILE_DATE_CACHE = {}

def GetFileDate(filename):
  if FILE_DATE_CACHE.has_key(filename):
    return FILE_DATE_CACHE[filename]

  d = GetFileDateFromFilename(filename)
  if not d:
    d = GetFileDateFromFileContents(filename)
  if d and YEAR_MONTH_RE.search(filename):
    FILE_DATE_CACHE[filename] = d
  return d

def GetFileDateFromFileContents(filename):
  d = None
  t = None

  index = filename.endswith('index.php')

  for line in file(filename):
    if index or d is None:
      m = BLOG_DATE_HEADER_RE.search(line)
      if m:
        d = datetime.date(int(m.group(4)), MONTHS[m.group(2)], int(m.group(3)))
    if index or d:
      m = BYLINE_RE.search(line)
      if m:
        t = datetime.time(int(m.group(2)), int(m.group(3)))
        if not index:
          break
  if d and t:
    dt =  datetime.datetime.combine(d, t)
    return dt
  else:
    logging.debug("Unable to find file date for: %s", filename)
    return None

def GetFileDateFromFilename(filename):
  match = FILENAME_DATE_RE.search(filename)
  if match:
    return datetime.date(int(match.group(1)),
                         int(match.group(2)),
                         int(match.group(2)))
  return None

def WriteCalendar(phps):
  year = int(phps[0][0:4])
  max_year = int(phps[-1][0:4]) + 1
  logging.info("Writing calendar for %r to %r", year, max_year)

  fn = os.path.join(ARCHIVE_DIR, 'index.php')
  fnout = open(fn, 'w')

  fnout.write('<span class="archive_calendar">')
  fnout.write('<span class="archive_calendar_start"></span>')
  for year in range(year, max_year):
    fnout.write('<span class="archive_calendar_year">%d</a></span>' % year)
    for i in range(1,13):
      #MONTHS[datetime.date(2008, i, 1).strftime('%B')] = i
      file_name = '%d_%02d_01_archive.php' % (year, i)
      m = datetime.date(2008, i, 1).strftime('%b')
      if file_name in phps:
        file_name = os.path.abspath(os.path.join(ARCHIVE_DIR, file_name))
        m = '<a href="%s">%s</a>' % (GetUrl(file_name), m)
      fnout.write('<span class="archive_calendar_month">%s</a></span>\n' % m)
      if i % 4 == 0:
        fnout.write('<span class="archive_calendar_quarter"></span>\n')
      if i % 6 == 0:
        fnout.write('<span class="archive_calendar_half_year"></span>\n')

  fnout.write('<span class="archive_calendar_end"></span></span>')
  fnout.close()

def WriteLabels(threshold=4):
  logging.info("Writing labels")
  labels = [x for x in os.listdir("labels")
            if x.endswith(".php") and x != 'index.php']

  # now count how many things in each label
  labels_tuples = [(x, CountBlogPosts(os.path.join('labels', x)))
                   for x in labels]


  logging.debug('found %d labels', len(labels_tuples))

  labels_tuples.sort(lambda x, y: y[1] - x[1])
  fn = 'labels/index.php'
  fnout = open(fn, 'w')

  fnout.write('<ul>\n')
  for (label, num) in labels_tuples:
    logging.debug('%s (%d)', label, num)
    if num >= threshold:
      file_name = os.path.abspath(os.path.join('labels', label))
      fnout.write('<li><a href="%s">%s</a> (%d)</li>\n' %
                  (GetUrl(file_name), label[0:-4].capitalize(), num))
  fnout.write('</ul>\n')
  fnout.close()

def CountBlogPosts(file_name):
  blog_posts = 0
  for line in open(file_name):
    if 'class="blogPost"' in line or 'class="post"' in line:
      blog_posts += 1
  return blog_posts

def NeedsToRun():
  files = (["index.php"] +
           [os.path.join(ARCHIVE_DIR, x)
            for x in os.listdir(ARCHIVE_DIR)
            if x.endswith(".php") and x != 'index.php'])

  for x in files:
    prevnext = x.replace(".php", ".prevnext")
    if (not os.path.exists(prevnext) or
        os.stat(x)[stat.ST_MTIME] >
        os.stat(prevnext)[stat.ST_MTIME]):
      return True
  return False

def Main(args):
  global debug_single
  force = False

  logging.basicConfig()
  logging.getLogger().setLevel(logging.INFO)

  try:
    opts, args = getopt.getopt(sys.argv[1:], 'hdb:fs:a:',
                               ['archive=',
                                'base=',
                                'debug',
                                'force',
                                'help',
                                'label_threshold=',
                                'skip=',
                                ])
  except getopt.error, msg:
    usage(1, msg)

  if args:
    usage(1)

  force = False
  label_threshold = 4
  for opt, arg in opts:
    if opt in ('-h', '--help'):
      usage(0)
    if opt in ('-d', '--debug'):
      logging.info("Setting logging level to debug")
      logging.getLogger().setLevel(logging.DEBUG)
      logging.debug("Setting logging level to debug")
    if opt in ('-b', '--base'):
      global BASE_URL
      BASE_URL = arg
    if opt in ('-f', '--force'):
      force = True
    if opt in ('-s', '--skip'):
      global FLAG_SKIP
      FLAG_SKIP = [x.strip().lower() for x in arg.split(',')]
    if opt in ('-a', '--archive'):
      global ARCHIVE_DIR, ARCHIVE_DIRS
      ARCHIVE_DIR = arg
      ARCHIVE_DIRS = re.compile("/" + ARCHIVE_DIR + "(/|$)")
    if opt == '--label_threshold':
      label_threshold = int(arg)

  if force or NeedsToRun():
    GetFileStatsFor(".")
    if 'labels' not in FLAG_SKIP:
      WriteLabels(threshold=label_threshold)
  else:
    logging.debug("nothing to do, force with -f")


if __name__ == '__main__':
  Main(sys.argv)
